Android 密码管理器:安全存储与 MFA 实战
之前一篇文章《从1password到自建密码管理器:2password实战指南》,介绍了用 PyQt5 + MySQL 构建桌面版密码管理器的完整过程。桌面版运行稳定,但在实际使用中,手机端使用也很频繁。
于是我开始着手 2password 的 Android 版本,目标是实现与桌面版同等安全级别的移动端密码管理方案。
相比桌面版,Android 版密码管理器有其独特的优势:
- 硬件级安全:利用 Android KeyStore 实现硬件级密钥保护,这是桌面端难以实现的
- 生物识别集成:原生支持指纹和面容识别,解锁更便捷
- 系统级自动锁定:利用应用生命周期实现自动锁定,安全无死角
- 现代 UI 框架:Jetpack Compose 提供声明式 UI 开发体验,代码更简洁
今天这篇文章,我将分享如何构建一个功能完善的 Android 密码管理器。如果你读过我的 2password 文章,会发现这两个项目在设计理念上有很多相似之处,但在技术实现上又有各自的特点。先看看效果!

一、为什么从桌面版到移动版?
在开始之前,先聊聊为什么我要从桌面版的 2password 扩展到 Android 版本。
在《2password 实战指南》那篇文章中,我分享了一个基于 PyQt5 + MySQL 的桌面密码管理器。实际使用后,我发现了一些痛点:
- 移动场景需求:很多时候我在手机上浏览网页,需要快速复制密码,但桌面版无法直接使用
- 安全性限制:桌面版的密码存储在本地 MySQL,虽然实用但缺乏硬件级密钥保护
- 生物识别缺失:桌面版没有原生生物识别支持,每次都要输入主密码略显繁琐
选择 Android 平台作为移动端方案,主要基于以下考虑:
- 生态成熟:Jetpack Compose 提供现代化的 UI 开发体验,代码量比 PyQt5 更少
- 安全框架:Android KeyStore 提供硬件级密钥保护,这是桌面端难以实现的安全特性
- 生物识别:原生支持指纹和面容识别,解锁更便捷
- 学习价值:从桌面到移动,是一次很好的技术栈拓展
两个版本的设计理念是一致的:简单实用、安全第一、数据自主。但在技术实现上,Android 版充分利用了平台特性,带来了更好的安全性和使用体验。
二、整体架构设计
密码管理器采用 MVVM 架构,结合 Jetpack Compose 构建 UI。整体架构如下:
三、核心功能模块
密码管理器的功能围绕"安全、便捷、可追溯"的原则设计。每个功能模块都经过精心打磨,确保既满足日常使用需求,又不牺牲安全性。
四、密码添加流程
密码添加是一个涉及验证、加密、存储的完整流程。看似简单的"添加密码"操作,背后其实有一套严密的安全机制在运行。
五、数据库设计
使用 Room 数据库,设计了两个核心表:密码表和历史表。每个密码都对应一个历史记录集合,这种设计确保了数据的完整性和可追溯性。
六、安全加密实现
密码安全是核心功能。使用 Android KeyStore 生成并存储密钥,配合 AES-256-GCM 算法加密数据。这里有几个关键的安全细节值得注意。
object EncryptionManager {
private const val KEY_ALIAS = "password_manager_key"
private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
private const val TRANSFORMATION = "AES/GCM/NoPadding"
private val keyStore: KeyStore by lazy {
KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) }
}
private fun createKey(): SecretKey {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
KEYSTORE_PROVIDER
)
val spec = KeyGenParameterSpec.Builder(KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.setRandomizedEncryptionRequired(true) // 每次加密使用随机IV
.build()
return keyGenerator.generateKey()
}
fun encrypt(data: String): EncryptedData {
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
val encryptedBytes = cipher.doFinal(data.toByteArray(Charsets.UTF_8))
val iv = cipher.iv
return EncryptedData(
ciphertext = Base64.encodeToString(encryptedBytes, Base64.NO_WRAP),
iv = Base64.encodeToString(iv, Base64.NO_WRAP)
)
}
}
七、生物识别与自动锁定
为了提升安全性和便捷性,应用集成了生物识别和自动锁定功能:
@Singleton
class BiometricAuthManager @Inject constructor() {
enum class BiometricStatus {
AVAILABLE,
NO_HARDWARE,
HARDWARE_UNAVAILABLE,
NONE_ENROLLED,
SECURITY_UPDATE_REQUIRED
}
fun checkBiometricAvailability(context: Context): BiometricStatus {
val biometricManager = BiometricManager.from(context)
return when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
BiometricManager.BIOMETRIC_SUCCESS -> BiometricStatus.AVAILABLE
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> BiometricStatus.NO_HARDWARE
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> BiometricStatus.HARDWARE_UNAVAILABLE
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> BiometricStatus.NONE_ENROLLED
else -> BiometricStatus.NO_HARDWARE
}
}
suspend fun authenticate(
activity: FragmentActivity,
title: String = "验证身份",
subtitle: String = "使用生物识别解锁密码管理器"
): Result<Boolean> = suspendCancellableCoroutine { continuation ->
val biometricPrompt = BiometricPrompt(
activity,
ContextCompat.getMainExecutor(activity),
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
continuation.resume(Result.success(true))
}
override fun onAuthenticationFailed() {
continuation.resume(Result.success(false))
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
continuation.resume(Result.failure(BiometricAuthException(errorCode, errString.toString())))
}
}
)
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setNegativeButtonText("取消")
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
.build()
biometricPrompt.authenticate(promptInfo)
}
}
八、MFA 验证码生成
支持 TOTP (Time-based One-Time Password) 算法,与 Google Authenticator 等应用完全兼容。这意味着你可以用一个密码管理器同时管理密码和 MFA,无需多个应用切换。
TOTP 的工作原理
TOTP 基于时间生成一次性密码,每 30 秒变化一次。服务端和客户端使用相同的密钥和当前时间计算验证码,只要时间同步,就能产生相同的验证码。RFC 6238 是这一算法的标准规范。
object TotpGenerator {
private const val HMAC_SHA1 = "HmacSHA1"
private const val DEFAULT_DIGITS = 6
private const val DEFAULT_PERIOD = 30
fun generateTotpCode(secret: String, digits: Int = DEFAULT_DIGITS): String {
if (secret.isEmpty()) return ""
val key = decodeBase32(secret)
val time = System.currentTimeMillis() / 1000 / DEFAULT_PERIOD
return generateOtp(key, time, digits)
}
private fun generateOtp(key: ByteArray, time: Long, digits: Int): String {
val data = ByteBuffer.allocate(8).putLong(time).array()
val signKey = SecretKeySpec(key, HMAC_SHA1)
val mac = Mac.getInstance(HMAC_SHA1)
mac.init(signKey)
val hash = mac.doFinal(data)
val offset = hash[hash.size - 1].toInt() and 0xf
val truncatedHash = ((hash[offset].toInt() and 0x7f) shl 24) or
((hash[offset + 1].toInt() and 0xff) shl 16) or
((hash[offset + 2].toInt() and 0xff) shl 8) or
(hash[offset + 3].toInt() and 0xff)
val otp = truncatedHash % (10.0.pow(digits.toDouble()).toInt())
return otp.toString().padStart(digits, '0')
}
}
九、历史记录追踪
所有密码修改都会自动记录历史,支持查看和恢复旧值。历史记录采用不可删除设计,确保审计完整性:
十、数据导入导出
支持加密格式的数据导入导出,方便在设备间迁移,防止删除应用时数据丢失。
备份的重要性
再好的软件也可能出问题,手机也可能丢失或损坏。定期备份数据是保护自己资产的重要习惯。导出的数据文件也是加密的,即使文件被窃取也无法读取内容。
十一、主题定制
为了提供个性化的使用体验,应用支持火主题:
private val LightColorScheme = lightColorScheme(
primary = Color(0xFFD32F2F), // 火主色
primaryContainer = Color(0xFFFFE0E0), // 浅红色容器
secondary = Color(0xFFFF5722), // 橙红色辅助色
secondaryContainer = Color(0xFFFFCCBC), // 橙色容器
tertiary = Color(0xFFE64A19), // 深红色第三色
background = Color(0xFFFFFBFE), // 背景色
surface = Color(0xFFFFFBFE), // 表面色
onPrimary = Color.White, // 主色文字
onSecondary = Color.White, // 辅助色文字
onTertiary = Color.White, // 第三色文字
onBackground = Color(0xFF1C1B1F), // 背景文字
onSurface = Color(0xFF1C1B1F) // 表面文字
)
@Composable
fun PasswordManagerTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = false, // 禁用 Material You
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
content = content
)
}
关键点:设置 dynamicColor = false 禁用 Material You 动态颜色,确保自定义主题生效
十二、安全最佳实践
通过这个项目,我总结了一些移动端密码管理器开发的最佳实践。这些经验不仅适用于密码管理器,也适用于任何需要处理敏感数据的 Android 应用:
| 安全维度 | 推荐做法 | 本项目实现 |
|---|---|---|
| 密钥存储 | 使用 Android KeyStore 硬件保护 | ✅ KeyStore + AES-256-GCM |
| 加密算法 | AES-256-GCM(认证加密) | ✅ GCM 模式 + IV 随机化 |
| 数据传输 | 禁用应用备份 | ✅ allowBackup=false |
| 访问控制 | 生物识别 + 自动锁定 | ✅ BiometricAuthManager |
| 数据完整性 | 历史记录不可删除 | ✅ 历史表无删除接口 |
| MFA 保护 | MFA 密钥加密存储 | ✅ mfaSecret 字段加密 |
| 数据备份 | 提供导出备份功能 | ✅ 加密格式导出/导入 |
| 主题保护 | 禁用动态颜色覆盖 | ✅ dynamicColor=false |
十三、Android 版 vs 桌面版对比
经过这次实践,我想对比一下两个版本的差异,帮助大家根据自己的需求选择合适的方案:
| 特性 | 2password (桌面版) | Android 版 |
|---|---|---|
| 技术栈 | Python + PyQt5 + MySQL | Kotlin + Jetpack Compose + Room |
| 数据存储 | 本地 MySQL 数据库 | Room SQLite 数据库 |
| 加密方式 | cryptography (AES-256) | Android KeyStore + AES-256-GCM |
| 生物识别 | ❌ 无原生支持 | ✅ 指纹/面容识别 |
| 自动锁定 | ✅ 超时锁定 | ✅ 后台自动锁定 |
| MFA 支持 | ✅ TOTP 生成 | ✅ TOTP 生成 |
| 历史记录 | ✅ 不可删除 | ✅ 不可删除 |
| 数据迁移 | ✅ CSV 导入导出 | ✅ 加密格式导入导出 |
| 安全性 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ (硬件级保护) |
| 便捷性 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ (生物识别) |
我的使用体验:
- 桌面版适合:在电脑前长时间工作,需要批量管理密码的场景
- Android 版适合:移动场景下的快速查看和复制密码,随时随地可用
- 两个版本的数据是独立的,我目前同时使用,数据通过加密备份文件手动同步
十四、项目总结
这个自建的密码管理器完全满足我的日常需求:
- - 本地存储,数据完全自主可控
- - 硬件级加密,安全性有保障
- - MFA 支持,与所有 TOTP 应用兼容
- - 历史记录,所有操作可追溯
- - 数据备份,防止删除应用时数据丢失
- - 生物识别,便捷安全的解锁方式
- - 自动锁定,保护应用免受未授权访问
如果你也在考虑自建密码管理器,不妨从 Android 平台开始。这是一个实用性强的项目,既能学到知识,又能解决实际问题。
最后提醒:密码安全无小事。无论使用哪种密码管理器,请确保:
使用强密码(长度至少 12 位,包含大小写、数字、特殊字符)
- 为每个网站使用不同的密码
- 启用 MFA 双因素认证
- 定期备份数据
- 不要将密码分享给他人
相关阅读
- 《从1password到自建密码管理器:2password实战指南》- 桌面版密码管理器完整实现
📜 版权声明
本文作者:王梓 | 原文链接:https://www.bthlt.com/note/10015-密码管理器 2password 安卓实现
出处:葫芦的运维日志 | 转载请注明出处并保留原文链接


📜 留言板
留言提交后需管理员审核通过才会显示